package com.xxx.ratelimiter;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

/**
 * 滑动窗口限流算法：
 * 实现方案：将每一个周期分割为多个窗口，当一个周期结束时，只将整个周期的开始时刻移动一个窗口的位置，这样就可以防止上述本地计数器算法那种临界点瞬间大流量的冲击问题。
 * 优点：相比计数器限流算法，滑动窗口算法会更加平滑，能自动消除毛刺。（可以将计数器限流算法看做是滑动窗口限流算法的特例：只划分了1格）
 * 缺点：如果如果某一瞬时并发多个请求，超过限流阀值时，直接被限流了，而不是允许部分请求可以被执行。
 */
public class SlideWindowLimiter {
	
	private static final int INTERVAL_SECONDS = 10; // 滑动时间窗口：10s
	private static final long LIMIT = 100; // 滑动时间窗口内的限流阈值：100次
	// 上述2个参数的含义：任意10s的时间窗口内最多请求次数限流阀值为100（也可以定义在每个限流api参数里）

	// 使用guava本地缓存做请求次数计数器：cache的key为api名+当前时间戳（秒），value为调用次数
	private static LoadingCache<String, AtomicInteger> counter = CacheBuilder.newBuilder()
										.expireAfterWrite(15, TimeUnit.SECONDS)  // 15秒后如果cache没有被更新就会被回收
										.build(new CacheLoader<String, AtomicInteger>() {
											
											@Override
											public AtomicInteger load(String seconds) throws Exception {
												return new AtomicInteger(0); // 重置计数器为0
											}
											
										});

	/**
	 * 判断是否被限流
	 * @param apiKey
	 * @return true：未被限流，false：被限流
	 */
	public static boolean slideWindowLimit(String apiKey) {
		try {
			long time = System.currentTimeMillis() / 1000; 		// 当前时间戳（秒）
			int currReqNum = (int) (Math.random() * 20) + 1; 	// 模拟每秒发送随机数量的请求（取值范围：[1,21]）
			counter.get(apiKey+time).addAndGet(currReqNum);  	// 累加本次请求次数（原子操作）
			
			// 累加前INTERVAL_SECONDS秒内每秒的请求数
			long totalReqNum = 0;
			for (int i=0; i<INTERVAL_SECONDS; i++) {
				totalReqNum += counter.get(apiKey+(time-i)).get();
			}
			
			// 判断INTERVAL_SECONDS秒内的总的请求次数是否达到限流阈值
			if (totalReqNum>LIMIT) { // 请求数超过了限流阈值
				System.out.println("apiKey=" + apiKey + ",time=" + time + ",currReqNum=" + currReqNum + ",totalReqNum=" + totalReqNum +",被限流了");
				return false;
			}
			
			// 未被限流
			System.out.println("apiKey=" + apiKey + ",time=" + time + ",currReqNum=" + currReqNum + ",totalReqNum=" + totalReqNum);
			return true;
		} catch (Exception e) {
			System.err.println(e);
		}
		return false;
	}
	
	// 获取当前这1秒内请求次数
	public static int getCurrInvokeTimes(String apiKey) {
		long time = System.currentTimeMillis() / 1000; // 当前时间戳（秒）
		try {
			return counter.get(apiKey+time).get();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
		return 0;
	}
	
	// 获取时间窗口内总的请求次数
	public static int getTotalInvokeTimes(String apiKey) {
		long time = System.currentTimeMillis() / 1000; // 当前时间戳（秒）
		try {
			// 累加前5秒内每秒的请求数
			Integer totalReqNum = 0;
			for (int i=0; i<INTERVAL_SECONDS; i++) {
				totalReqNum += counter.get(apiKey+(time-i)).get();
			}
			return totalReqNum;
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
		return 0;
	}
	
	// 测试
	public static void main(String[] args) {
		// 使用调度线程池模拟3个并发请求线程，线程池初始化完成2s后启动，每1s滑动一次窗口
		Executors.newScheduledThreadPool(3).scheduleWithFixedDelay(
				() -> {
					SlideWindowLimiter.slideWindowLimit("testApi");
				}, 
				2,
				1,
				TimeUnit.SECONDS);
    }
	
}
